
module(..., package.seeall)

ENABLE_LOGGING = true
--SAPI.Response.contenttype("text/plain")

require("lfs")
require("md5") 
require("logging.file")
require("cosmo")
require("versium.smart.repository")
require("sputnik")
require("sputnik.markup")
require("sputnik.luaenv")
require("sputnik.actions.wiki")
require("sputnik.authentication.simple")
require("sputnik.i18n")

-------------------------------------------------------------------------------
--- THE SPUTNIK CLASS ---------------------------------------------------------
-------------------------------------------------------------------------------

Sputnik = {}

--- Creates a new instance of Sputnik.

function Sputnik.new(self, initial_config)
   local obj = {}
   setmetatable(obj, self)
   self.__index = self
   obj:init(initial_config)
   return obj
end


--- Initializes a the new Sputnik instance.

function Sputnik.init(self, initial_config)
   self.config = initial_config

   if ENABLE_LOGGING or config.ENABLE_LOGGING then
      self.logger = logging.file("/tmp/sputnik-log.log", "%Y-%m-%d")
   else
      self.logger = logging.new(function(self, level, message) end)-- does nothing
   end

   -- setup storage
   INITIAL_CONFIG = {}
   INITIAL_CONFIG.VERSIUM_PARAMS = {dir = "/home/yuri/tmp/sputnik-2007-12-05c/wiki-data"}
   INITIAL_CONFIG.ROOT_PROTOTYPE="@Root"
   self.repo = versium.smart.repository.Repository:new(INITIAL_CONFIG)
   self.repo.suppress_activation = true
      
   -- extra configurations
   for k,v in pairs(self:load_config_page(self.config.CONFIG_PAGE_NAME,{BASE_URL = self.config.BASE_URL})) do
      self.config[k] = v
   end
   
   -- setup markup
   local markup_module = require(initial_config.MARKUP_MODULE or "sputnik.markup.markdown")
   local markup = markup_module.open()
   self.markup = sputnik.markup.open(markup)
   self.markup_module = markup_module
   
   -- setup the translator
   local translations = self:load_config_page(self.config.TRANSLATIONS_PAGE)
   self.translator = sputnik.i18n.make_translator(translations, self.config.INTERFACE_LANGUAGE)
      
   -- setup authentication
   self.auth = sputnik.authentication.simple.make_authenticator(self)
   
   -- setup actions
   self.actions = sputnik.actions.wiki.actions   -- "wiki" actions are the default
   self.actions.__index = self.actions           -- to use it as a metatable later
   self.wrappers = sputnik.actions.wiki.wrappers -- same for "wiki" wrappers      
end

--- Loads a module with additional actoins.

function Sputnik.load_extra_actions(self, mod_name)
   local m = require("sputnik.actions." .. mod_name)
   return m.actions
end

--- Splits a string on a delimiter.
--
-- Adapted from http://lua-users.org/wiki/SplitJoin.

function Sputnik.split (self, text, delimiter)
   local list = {}
   local pos = 1
   if string.find("", delimiter, 1) then 
      error("delimiter matches empty string!")
   end
   while 1 do
      local first, last = string.find(text, delimiter, pos)
      if first then -- found?
         table.insert(list, string.sub(text, pos, first-1))
         pos = last+1
      else
         table.insert(list, string.sub(text, pos))
         break
      end
   end
   return unpack(list)
end

--- Escapse a text for using in a textarea.

function Sputnik.escape(self, text) 
   return (text or ""):gsub("&", "&amp;"):gsub(">","&gt;"):gsub("<","&lt;")
end

--- Escapes a URL.

function Sputnik.escape_url (self, text) 
   return self:escape(text):gsub(" ","%%20")
end


--- Wikifies a text string.

function Sputnik.wikify_text (self, text, templates)
   return self.markup:wikify_text (text, templates, self.config.NICE_URL, self)
end

--- Makes a wikilink from a table.

function Sputnik.format_wikilink(self, title, link, exists, templates)
   local tab = { title = title, link = link}
   return cosmo.fill(templates.WIKI_LINK,
           {  if_page_exists = cosmo.cond(exists, tab),
         if_no_such_page = cosmo.cond(not exists, tab) }
        )
end

--- Turns a string into something that can be used as a page name.

function Sputnik.dirify(self, text)
   -- convert ? + = % ' " / \ and spaces to '_'
   -- note this isn't enought to ensure legal win2k names, since we
   -- _are_ allowing [ ] + < > : ; *
   return (text or ""):gsub([[[%?%+%=%%%s%'%"%/%\]+]], "_")
end

--- Makes a URl from a table of parameters.

function Sputnik.make_url(self, page_name, action, params)
   page_name = self:dirify(page_name)
   local page_dot_action
   if action then 
      page_dot_action = page_name .. "." .. action
   else
      page_dot_action = page_name
   end
   if params and next(params) then
      local link = self.config.BASE_URL .. "?p=" .. page_dot_action
      for k, v in pairs(params or {}) do
         link = link .. "&" .. k .. "=" .. v
      end
      return self:escape(link)
   else
      return self:escape(self.config.NICE_URL .. page_dot_action)
   end
end

--- Makes a link from a table of parameters.

function Sputnik.make_link(self, page_name, action, params)
   local url = self:make_url(page_name, action, params)
   return string.format("href='%s' class='local'", url)
end

--- Adds to a page all it needs to be used by actions.
--
-- Will need to make a somewhat complicated chain of metatables,
-- so that we can have access to three tables representing the page:
--
-- 1. The table representing the page as it stored ("as_is")
-- 2. A table where page fields have been concatenated
--    with the necessary fields from the prototypes ("inherited")
-- 3. A version where the necessary parameters have been converted
--    from just strings to Lua tables and functions ("active_page")
--
-- We end up with the following chain of metatables
--
--     active_page -> inherited -> as_is


function Sputnik.decorate_page(self, page, params)
   params = params or {}
   page = self:activate_page(page, params)
   return page
end

--- Applies inheritance form page's prototype.
--
-- (Chains together defaulted values from the prototypes of this page.)

function Sputnik.apply_inheritance_from_prototype(self, page) 
               
   self.config.ROOT_PROTOTYPE = self.config.ROOT_PROTOTYPE or "@Root"
   -- check if it's time to break the recursion
   if page.name == self.config.ROOT_PROTOTYPE then
      return page
   end

   -- Create a new page and make the original page it's metatable. 
   -- This way we'll still have access to the old values when we 
   -- need them (e.g., for the edit forms).
   
   page.__index = page
   local page = {as_is = page}
   setmetatable(page, page.as_is)

   -- "page" now stores the new page, which falls back onto the original
   -- (The original page is now in page.as_is)
   
   -- Load the prototype, together with its own prototypes

   page.prototype = page.prototype or self.config.ROOT_PROTOTYPE
   local prototype = self:get_page(page.prototype)
   prototype = self:apply_inheritance_from_prototype(prototype)
   
   -- Do the current level of augmentation, using the information in 
   -- the 'fields' field to decide how to handle each field.  Note
   -- that we use this page's "fields" table rather than the fields
   -- table from the prototype.  However, the "fields" page itself must 
   -- _always_ be inherited as a matter of bootstrapping.
   
   -- An auxilary function to concat field values.  Note that it
   -- inserts an extra "\n" between the values, to make sure that 
   -- we can contatenate stretches of Lua code.

   local function concat(x,y)
      return (x or "") .. "\n" .. (y or "")
   end

   page.fields = concat(prototype.fields, page.fields)
   
   local fields, err = self:load_config(page.fields, {})
   
   for field_name, field in pairs(fields) do
      field.name = field_name
      if field.proto then
    -- "concat" is the only prototype handling method that we
    -- have for now, but maybe we'll have more later.  
    if field.proto == "concat" then
       page[field.name] = concat(prototype[field.name],
                  page[field.name])
    end
      end
   end
   return page
end

--- Turns string parameters into Lua functions and tables, making them 
--- callable.  The original values will be available in .as_is and 
--- .inherited.

function Sputnik.activate_page(self, page, params) 

   -- Create a new table to store the "active page".  Set the
   -- original page as it's metatable.
   
   page.__index = page
   local active_page = {inherited = page}
   setmetatable(active_page, page)
   
   -- Config (make sputnik.config a fallback) ---------------
   
   active_page.config = self:load_config(page.config)
   self.config.__index = self.config
   setmetatable(active_page.config, self.config)

   -- Parse fields again (this time is for saving)
   
   active_page.fields, err = self:load_config(page.fields, {})
   
   -- Load templates and put them into active_page.templates
   
   active_page.templates = {}
   
   local templates_before_translation = self:load_config_page(
            active_page.config.DEFAULT_TEMPLATE_SET)
   --SAPI.Response.contenttype("text/plain")
   if page.templates then
      --print(page.templates)
      for template_page in string.gmatch(page.templates, "([^%s]+)") do
         --print(template_page)
         for k, v in pairs(self:load_config_page(template_page)) do
       templates_before_translation[k] = v
         end
      end
   end
   
   -- now translate them
   for k, raw_template in pairs(templates_before_translation) do
      active_page.templates[k] = self.translator.translate(raw_template)
   end
   
   -- Actions -----------------------------------------------
   
   if page.actions and type(page.actions) == "string" then
      active_page.actions = self:load_config(page.actions)
   else
      active_page.actions = {}
   end
      
   -- self.actions is the actions we loaded from sputnik.actions
   -- it's not the same as active_page.actions, which is just a table
   -- of function names
   
   local actions = {}
   setmetatable(actions, self.actions)
   
   for k, v in pairs(active_page.actions) do
      local mod_name, dot_action = self:split(v, "%.")
      if dot_action then
         actions[k] = self:load_extra_actions(mod_name)[dot_action]
      else
         actions[k] = actions[v]
      end
   end

   active_page.actions = actions
   
   -- Permissions -------------------------------------------
   
   active_page.check_permissions = function(user, action)
      local state = true
      local all = true -- just a constant          
      local function set(id, some_action, value)
         if (some_action == action) then
            if (type(id)=="string" and id==user) or id==true then 
               state = value
            end
         end
      end         
      local function allow(some_user, some_action) 
         set(some_user, some_action, true)
      end
      local function deny(some_user, some_action)
         set(some_user, some_action, false)
      end
      self:load_config(page.permissions or "", {all = all, allow = allow, deny = deny} )
      return state
   end     
   
   -- Wrappers ----------------------------------------------

   active_page.wrappers = self.wrappers
   return active_page
end

--- Returns the page with this name (undecorated).

function Sputnik.get_page(self, page_name, version)
   local page = self.repo:get_node(page_name, version)
   page.name = page_name
   self:add_urls(page)
   self:add_links(page)
   return page
end

--- Adds a "urls" fields to a page.  
--
--  This makes page.urls:foo(params) equivalent to 
--  sputnik:make_url(page.name, "foo", params) for ANY foo.
----------------------------------------------------------------------------

function Sputnik.add_urls(self, page)
   page.urls = {}
   setmetatable(page.urls,
      { __index = function(table, key)
                return function(inner_self, params)
                return self:make_url(page.name, 
                           key, 
                           params)
                  end
             end
      }
        )
   return page
end

--- Adds a "link" field to the page.  (See add_urls.)

function Sputnik.add_links(self, page)
   page.links = {}
   setmetatable(page.links,
      { __index = function(table, key)
                return function(inner_self, params)
                return self:make_link(page.name, 
                            key, 
                            params)
                  end
             end
      }
        )
   return page
end

--- Generates page-like table to make urls.

function Sputnik.pseudo_page(self, page_name)
   local page = {name = page_name }
   self:add_urls(page)
   self:add_links(page)
   return page
end

--- Updates page with values from params table.

function Sputnik.update_page_with_params(self, page, params)
   local new_page = { as_is = {} }
   params.author = params.user
   for field, field_params in pairs(page.fields) do
      if not field_params.virtual then
    new_page.as_is[field] = params[field] or page.as_is[field]
      end
   end
   local raw = self.storage:page_to_string(new_page, page.fields)
   new_page = self.storage:string_to_page(page.name, raw)
   self:add_urls(new_page)
   self:add_links(new_page)
   return new_page
end

--- Saves the page (takes undecorated page as a parameter).

function Sputnik.save_page(self, page, raw_page)
   if raw_page then
      return self.storage:save_page(page.name, raw_page)
   else
      return self.storage:save_page(page.name, page)
   end
end

--- Returns page history.

function Sputnik.get_history(self, page_name, limit, date)
   local pages = self.repo:get_node_history(page_name, date)  -- limit discarded for now
   for i, p in ipairs(pages) do
      self:add_urls(p)
      self:add_links(p)
   end
   return pages
end

--- Returns a list of all page names

function Sputnik.get_page_names(self)
   local page_ids = self.storage.storage:list_object_ids() -- reaching deep
   table.sort(page_ids)
   return page_ids
end

--- Parses safely Lua code in a string and use it as table.

function Sputnik.load_config(self, text, env)
   local sandbox = sputnik.luaenv.make_sandbox(env)
   sandbox.do_lua(text)
   return sandbox.values
end

---  Loads page and parses it's content as if it was a config.

function Sputnik.load_config_page(self, page_name, env)
   return self:load_config(self:get_page(page_name).content, env)
end


function Sputnik:hash_field_name(field_name, token)
   return "field_"..md5.sumhexa(field_name..token..self.config.SECRET_CODE)
end

function Sputnik:get_current_form_token()
   local timestamp = self.auth.now()
   return self.auth.get_token_for_timestamp(timestamp), timestamp
end

function Sputnik:check_form_token(token, timestamp)
   if not token then
      return "MISSING_POST_TOKEN"
   elseif not timestamp then
      return "MISSING_POST_TIME_STAMP"
   elseif (self.auth.hours_since_ce(self.auth.now())-self.auth.hours_since_ce(timestamp)) > 3 then
      return "YOUR_POST_TOKEN_HAS_EXPIRED"
   elseif self.auth.get_token_for_timestamp(timestamp)~=token then
      return "YOUR_POST_TOKEN_IS_INVALID"
   end
end

--- Processes parameters from the HTTP layer

-------------------------------------------------------------------------------
-- A function to handle the CGI parameters (and do the authentication) --------
-------------------------------------------------------------------------------

function Sputnik:translate_request (request)
   -- for a post action we'll need to unhash the parameters first
   if request.params.post_token and request.params.post_fields then 
      local new_params = {}
      for k,v in pairs(request.params) do
         if k:sub(0,7) == "action_" then
            new_params[k] = v
         end
      end
      for name in string.gmatch(request.params.post_fields, "%a+") do 
         new_params[name] = request.params[self:hash_field_name(name, request.params.post_token)]
      end
      new_params.p = request.params.p
      request.params = new_params
   end
   
   for k,v in pairs(request.params) do
      self.logger:info(k..": "..v)
   end
   
   -- break "p" parameter into page name and the action
   if request.params.p then
      request.page_name, request.action = self:split(request.params.p, "%.")
   else
      request.page_name = self.config.HOME_PAGE 
   end
   request.action = request.action or "show"

   -- now login/logout/register the user
   self.logger:info("authorization")
   self.logger:info(request.params.user)
   if request.params.logout then 
      self.logger:info(request.params.logout)
      request.user = nil
   elseif (request.params.user or ""):len() > 0 then
      self.logger:info(request.params.user or "")
      request.user, request.auth_token = self.auth.check_password(request.params.user, request.params.password)
   else
      local cookie = request.get_cookie(self.cookie_name) or ""
      self.logger:info(cookie)
      local user_from_cookie, auth_token = self:split(cookie, "|")
      if user_from_cookie then
         request.user = self.auth.check_token(user_from_cookie, auth_token)
         if request.user then
            request.auth_token = auth_token
         end
      end
   end
   self.logger:info(request.user)
         self.logger:info("-----------")
   return request
end



function Sputnik:run(request, response)
   self.cookie_name = "Sputnik_"..md5.sumhexa(self.config.BASE_URL)
   self.logger:info("========"..(request.p or "<default>").."==========")
   request = self:translate_request(request)
   local page = self:get_page(request.page_name, request.params.version)
   page = self:decorate_page(page, request)

   local action_function = page.actions[request.action or "show"]
                           or sputnik.actions.wiki.actions.action_not_found

   local content, content_type = action_function(page, request, self)
   response.set_cookie(self.cookie_name, (request.user or "").."|"..(request.auth_token or ""), {path="/"})
   response.set_content_type(content_type or "text/html")
   assert(content)     
   response.write(content)
end

--- END OF THE SPUTNIK CLASS --------------------------------------------------



         
                      

